package com.hzw.saas.common.storage.service.impl;

import java.io.*;
import java.util.List;
import java.util.Objects;

import com.hzw.saas.common.storage.enums.LocChunkStatusEnum;
import com.hzw.saas.common.storage.enums.PathTypeEnum;
import com.hzw.saas.common.storage.pojo.FileStorage;
import com.hzw.saas.common.storage.pojo.InxChunkMetadata;
import com.hzw.saas.common.storage.pojo.InxChunkStorage;
import com.hzw.saas.common.storage.pojo.LocChunkStorage;
import com.hzw.saas.common.storage.service.IChunkStorageService;
import com.hzw.saas.common.storage.service.IFileStorageService;
import com.hzw.saas.common.storage.util.StorageHubUtil;
import com.hzw.saas.common.util.PathUtils;
import com.hzw.saas.common.util.enums.ConflictModeEnum;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.date.SystemClock;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IORuntimeException;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.io.file.PathUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

/**
 * 分片存储默认实现
 *
 * @author zzl
 * @since 04/21/2021
 */
@Service
@RequiredArgsConstructor
@Slf4j
public class ChunkStorageServiceImpl implements IChunkStorageService {

    private final IFileStorageService fileStorageService;

    /**
     * 分片元数据信息，用于索引类型分片保存分片总数等
     */
    public static final String METADATA_FILE_NAME = "metadata";
    private static final String MERGE_TMP_FILE = "chunks.tmp";

    @Override
    public File getChunk(String path) {
        Assert.isTrue(StrUtil.isNotBlank(path), "path is blank");
        return FileUtil.file(path);
    }

    @Override
    public File getChunk(String supPath, String chunkName) {
        Assert.isTrue(StrUtil.isNotBlank(chunkName), "chunk name is blank");
        String path = PathUtils.connectPath(supPath, chunkName);
        return this.getChunk(path);
    }

    @Override
    public boolean exist(String path) {
        File chunk = this.getChunk(path);
        return FileUtil.exist(chunk);
    }

    @Override
    public boolean exist(String supPath, String chunkName) {
        String path = PathUtils.connectPath(supPath, chunkName);
        return this.exist(path);
    }

    @Override
    public long getLength(String path) {
        File chunk = this.getChunk(path);
        return StorageHubUtil.size(chunk);
    }

    @Override
    public long getLength(String supPath, String chunkName) {
        String path = PathUtils.connectPath(supPath, chunkName);
        return this.getLength(path);
    }

    @Override
    public String getDigest(String path, String digestType) {
        File chunk = this.getChunk(path);
        return StorageHubUtil.calcDigest(chunk, digestType);
    }

    @Override
    public String getDigest(String supPath, String chunkName, String digestType) {
        String path = PathUtils.connectPath(supPath, chunkName);
        return this.getDigest(path, digestType);
    }

    @Override
    public void saveChunk(String supPath, String chunkName, InputStream ins) {
        Assert.notNull(ins, "待保存分片丢失：".concat(chunkName));
        File destFile = this.getChunk(supPath, chunkName);
        log.debug("保存分片：{}", destFile);
        FileUtil.writeFromStream(ins, destFile, false);
    }

    @Override
    public void saveChunk(LocChunkStorage chunkInfo, byte[] chunkBytes) {
        // 参数校验
        Assert.notNull(chunkBytes, "save loc chunk: data to save is null.");
        Assert.notNull(chunkInfo, "save loc chunk: chunk info is null");
        // 校验源
        StorageHubUtil.checkSrcLength(chunkBytes, chunkInfo.getLength());
        StorageHubUtil.checkSrcDigest(chunkBytes, chunkInfo.getDigest(), chunkInfo.getDigestType());
        File chunk = this.getChunk(chunkInfo.getSuperPath(), chunkInfo.getChunkId());
        String chunkInfoStr = JSONUtil.toJsonStr(chunkInfo);
        // 保存
        log.debug("save the loc chunk: {}", chunkInfoStr);
        long start = SystemClock.now();
        FileUtil.writeBytes(chunkBytes, chunk);
        long costTime = SystemClock.now() - start;
        // 校验目标
        Long length = chunkInfo.getStartBytes() != null && chunkInfo.getEndBytes() != null ?
            chunkInfo.getEndBytes() - chunkInfo.getStartBytes() + 1 : chunkInfo.getLength();
        StorageHubUtil.checkTarLength(chunk, length, true);
        StorageHubUtil.checkTarDigest(chunk, chunkInfo.getDigest(), chunkInfo.getDigestType(), true);
        log.info("save the loc chunk completed (cost: {}ms): {}", costTime, chunkInfoStr);
    }

    @Override
    public void saveChunk(InxChunkStorage chunkInfo, byte[] chunkBytes) {
        // 参数校验
        Assert.notNull(chunkBytes, "save inx chunk: data to save is null.");
        Assert.notNull(chunkInfo, "save inx chunk: chunk info is null");
        Assert.isTrue(!ObjectUtil.hasNull(chunkInfo.getChunkNumber(), chunkInfo.getCurrentChunkSize(), chunkInfo.getTotalChunks()),
            "save inx chunk: chunk params has null");
        // 校验源
        StorageHubUtil.checkSrcLength(chunkBytes, chunkInfo.getCurrentChunkSize());
        StorageHubUtil.checkSrcDigest(chunkBytes, chunkInfo.getDigest(), chunkInfo.getDigestType());
        // 保存元数据
        if (Objects.equals(chunkInfo.getChunkNumber(), 1) ||
            (ObjectUtil.compare(chunkInfo.getTotalChunks(), 10) > 0 && Objects.equals(chunkInfo.getChunkNumber(), chunkInfo.getTotalChunks()))) {
            this.writeMetadata(chunkInfo);
        }
        // 保存分片数据
        File chunk = this.getChunk(chunkInfo.getSuperPath(), String.valueOf(chunkInfo.getChunkNumber()));
        String chunkInfoStr = JSONUtil.toJsonStr(chunkInfo);
        log.debug("save the inx chunk: {}", chunkInfoStr);
        long start = SystemClock.now();
        FileUtil.writeBytes(chunkBytes, chunk);
        long costTime = SystemClock.now() - start;
        // 校验目标
        StorageHubUtil.checkTarLength(chunk, chunkInfo.getCurrentChunkSize(), true);
        StorageHubUtil.checkTarDigest(chunk, chunkInfo.getDigest(), chunkInfo.getDigestType(), true);
        log.info("save the inx chunk completed (cost: {}ms): {}", costTime, chunkInfoStr);

    }

    @Override
    public void deleteChunk(String path) {
        File chunk = this.getChunk(path);
        log.debug("delete chunk: {}", chunk);
        FileUtil.del(chunk);
        log.info("delete chunk completed: {}", chunk);
    }

    @Override
    public void deleteChunk(String supPath, String chunkName) {
        String path = PathUtils.connectPath(supPath, chunkName);
        this.deleteChunk(path);
    }

    @Override
    public void deleteChunks(String supPath) {
        File chunksFolder = FileUtil.file(supPath);
        log.debug("delete chunks: {}", chunksFolder);
        PathUtil.del(chunksFolder.toPath());
        log.info("delete chunks completed: {}", chunksFolder);
    }

    @Override
    public void writeMetadata(String supPath, InxChunkMetadata inxChunkMetadata) {
        File file = this.getMetadataFile(supPath);
        String metadata = JSONUtil.toJsonStr(inxChunkMetadata);
        try {
            log.debug("写入分片元数据: {}", metadata);
            FileUtil.writeUtf8String(metadata, file);
            log.info("写入分片元数据完成: {}", metadata);
        } catch (Exception e) {
            throw new IORuntimeException(e, "写入分片元数据异常: {}", metadata);
        }
    }

    @Override
    public InxChunkMetadata readMetadata(String supPath) {
        File file = this.getMetadataFile(supPath);
        if (!FileUtil.exist(file))
            return null;
        try {
            String metadata = FileUtil.readUtf8String(file);
            return JSONUtil.toBean(metadata, InxChunkMetadata.class);
        } catch (Exception e) {
            throw new IORuntimeException(e, "读取分片元数据异常: {}", supPath);
        }
    }

    @Override
    public File mergeChunks(FileStorage fileInfo, String chunkSupPath, List<LocChunkStorage> chunkInfoList, boolean delChunks, ConflictModeEnum mode) throws IORuntimeException {
        Assert.notNull(fileInfo, "merge loc chunks: file info is null");
        log.debug("merge loc chunks ({}) ...", chunkSupPath);
        // 合并前检查
        this.mergeCheck(chunkSupPath);
        this.checkLocChunks(1, fileInfo.length(), chunkInfoList);
        // 目标文件重名检查
        File tarFile = this.fileStorageService.getFile(fileInfo.getSuperPath(), fileInfo.getFileName());
        tarFile = StorageHubUtil.doConflictName(tarFile, PathTypeEnum.FILE, false, mode);
        fileInfo.setFileName(tarFile.getName());
        try {
            // 可能存在的已合并的文件
            File file = this.fileStorageService.getFile(fileInfo.getSuperPath(), fileInfo.getFileName());
            // 合并分片
            File tmpFile = this.touchTmpFile(chunkSupPath);
            long costTime = this.mergeLocChunks(chunkSupPath, chunkInfoList, tmpFile, file);
            // 校验文件
            StorageHubUtil.checkTarLength(tmpFile, fileInfo.getLength(), true);
            StorageHubUtil.checkTarDigest(tmpFile, fileInfo.getDigest(), fileInfo.getDigestType(), true);
            // 完成合并
            tarFile = this.renameTmpFile(chunkSupPath, fileInfo.getSuperPath(), fileInfo.getFileName(), mode);
            fileInfo.setFileName(tarFile.getName());
            log.info("合并loc文件分片({})完成，耗时 {}ms", chunkSupPath, costTime);
            return tarFile;
        } finally {
            // 合并完成，删除分片
            this.deleteChunksAfterMerged(chunkSupPath, delChunks);
        }

    }

    @Override
    public File appendChunks(FileStorage fileInfo, String chunkSupPath, List<LocChunkStorage> chunkInfoList, boolean delChunks, ConflictModeEnum mode) throws IORuntimeException {
        Assert.notNull(fileInfo, "append loc chunks: file info is null");
        log.debug("append loc chunks ({})...", chunkSupPath);
        // TODO: ZZL@04/22/2021 暂不实现，功能已取消
        return null;
    }

    @Override
    public File mergeChunks(FileStorage fileInfo, String chunkSupPath, boolean delChunks, ConflictModeEnum mode) throws IORuntimeException {
        Assert.notNull(fileInfo, "merge inx chunks: file info is null");
        log.debug("merge loc chunks ({})...", chunkSupPath);
        // 合并前检查
        this.mergeCheck(chunkSupPath);
        InxChunkMetadata chunkMetadata = this.checkInxChunks(fileInfo, chunkSupPath);
        // 目标文件重名检查
        File tarFile = this.fileStorageService.getFile(chunkMetadata.getSuperPath(), chunkMetadata.getFileName());
        tarFile = StorageHubUtil.doConflictName(tarFile, PathTypeEnum.FILE, false, mode);
        fileInfo.setFileName(tarFile.getName());
        try {
            // 合并分片
            File tmpFile = this.touchTmpFile(chunkSupPath);
            long costTime = this.mergeInxChunks(chunkSupPath, chunkMetadata.getTotalChunks(), tmpFile);
            // 校验文件
            StorageHubUtil.checkTarLength(tmpFile, chunkMetadata.getLength(), true);
            StorageHubUtil.checkTarDigest(tmpFile, chunkMetadata.getDigest(), chunkMetadata.getDigestType(), true);
            // 完成合并
            tarFile = this.renameTmpFile(chunkSupPath, chunkMetadata.getSuperPath(), chunkMetadata.getFileName(), mode);
            fileInfo.setFileName(tarFile.getName());
            log.info("合并inx文件分片({})完成，耗时 {}ms", chunkSupPath, costTime);
            return tarFile;
        } finally {
            // 合并完成，删除分片
            this.deleteChunksAfterMerged(chunkSupPath, delChunks);
        }
    }

    // ================================================ private method ================================================ //

    /**
     * 分片合并前需求元素检查
     */
    private void mergeCheck(String chunkSupPath) {
        // 分片目录检查
        if (!FileUtil.isDirectory(chunkSupPath)) {
            throw new IORuntimeException(new FileNotFoundException("chunks folder not found：" + chunkSupPath));
        }
    }

    /**
     * LOC 型分片合并
     */
    private long mergeLocChunks(String chunkSuperPath, List<LocChunkStorage> chunkInfoList, File tmpFile, File tarFile) {
        // 计时开始
        long start = SystemClock.now();
        try (
            RandomAccessFile fileRa = tarFile.exists() ? new RandomAccessFile(tarFile, "r") : null;
            OutputStream out = FileUtil.getOutputStream(tmpFile)
        ) {
            for (LocChunkStorage chunkInfo : chunkInfoList) {
                byte[] chunkBytes = new byte[0];
                if (ObjectUtil.equal(chunkInfo.getStatus(), LocChunkStatusEnum.MERGING.code())) {
                    File chunk = this.getChunk(chunkSuperPath, chunkInfo.getChunkId());
                    if (!FileUtil.exist(chunk)) {
                        String errorMsg = String.format("[%s] Loc chunk not found: %s", chunkSuperPath, chunkInfo.getChunkId());
                        throw new IORuntimeException(new FileNotFoundException(errorMsg));
                    }
                    chunkBytes = FileUtil.readBytes(chunk);
                }
                if (ObjectUtil.equal(chunkInfo.getStatus(), LocChunkStatusEnum.MERGED.code())) {
                    if (fileRa == null)
                        throw new IORuntimeException(new FileNotFoundException("原文件不存在：" + tarFile));
                    int offset = chunkInfo.getStartBytes().intValue() - 1;
                    int length = (int) (chunkInfo.getEndBytes() - chunkInfo.getStartBytes()) + 1;
                    chunkBytes = new byte[length];
                    fileRa.seek(offset);
                    fileRa.read(chunkBytes, 0, length);
                }
                IoUtil.write(out, false, chunkBytes);
            }
            IoUtil.flush(out);
        } catch (IORuntimeException e) {
            throw e;
        } catch (Exception e) {
            throw new IORuntimeException(e);
        }
        // 计时结束
        return SystemClock.now() - start;
    }

    /**
     * INX 型分片合并
     */
    private long mergeInxChunks(String chunkSuperPath, int totalChunks, File tmpFile) {
        // 计时开始
        long start = SystemClock.now();
        try (
            OutputStream out = FileUtil.getOutputStream(tmpFile)
        ) {
            for (int i = 1; i <= totalChunks; i++) {
                log.debug("merge file chunk：No.{}...", i);
                File chunk = this.getChunk(chunkSuperPath, String.valueOf(i));
                if (!FileUtil.exist(chunk)) {
                    String errorMsg = String.format("[%s] Inx chunk not found: No.%s", chunkSuperPath, i);
                    throw new IORuntimeException(new FileNotFoundException(errorMsg));
                }
                IoUtil.write(out, false, FileUtil.readBytes(chunk));
            }
            IoUtil.flush(out);
        } catch (IORuntimeException e) {
            throw e;
        } catch (Exception e) {
            throw new IORuntimeException(e);
        }
        // 计时结束
        return SystemClock.now() - start;
    }

    /**
     * 合并完成删除分片文件
     */
    private void deleteChunksAfterMerged(String chunkSuperPath, boolean delChunks) {
        // 合并完成，删除分片
        if (delChunks) {
            try {
                this.deleteChunks(chunkSuperPath);
            } catch (Exception ignored) {}
        }
    }

    // ================================================ 合并临时文件工具 ================================================ //

    private File getMetadataFile(String chunkSupPath) {
        String metadataPath = PathUtils.connectPath(chunkSupPath, METADATA_FILE_NAME);
        return FileUtil.file(metadataPath);
    }

    private File getTmpFile(String chunkSuperPath) {
        return FileUtil.file(chunkSuperPath, MERGE_TMP_FILE);
    }

    private File touchTmpFile(String chunkSuperPath) {
        File tmpFile = this.getTmpFile(chunkSuperPath);
        log.debug("创建分片合并临时文件: {}", tmpFile);
        return FileUtil.touch(tmpFile);
    }

    private File renameTmpFile(String chunkSuperPath, String fileSuperPath, String fileName, ConflictModeEnum mode) {
        File tmpFile = this.getTmpFile(chunkSuperPath);
        if (!tmpFile.exists())
            throw new IORuntimeException(new FileNotFoundException("分片合并临时文件丢失: " + tmpFile.toString()));
        File tarFile = this.fileStorageService.getFile(fileSuperPath, fileName);
        return this.fileStorageService.move(tmpFile.getAbsolutePath(), tarFile.getAbsolutePath(), mode);
    }

    // ================================================ 定位型分片工具 ================================================ //

    /**
     * 对分片列表按起始索引升序排序，并校验分片是否连续
     *
     * @param startIndex  开始索引
     * @param fileLength  文件长度
     * @param chunkBOList 分片信息
     */
    private void checkLocChunks(long startIndex, long fileLength, List<LocChunkStorage> chunkBOList) {
        // 对分片列表按起始索引升序排序
        CollectionUtil.sort(chunkBOList, (o1, o2) -> ObjectUtil.compare(o1.getStartBytes(), o2.getStartBytes()));
        String chunksInfo = JSONUtil.toJsonStr(chunkBOList);
        log.debug("分片信息校验：{}", chunksInfo);
        // 连续性校验
        for (LocChunkStorage chunkInfo : chunkBOList) {
            // 判断分片是否连续
            if (ObjectUtil.notEqual(chunkInfo.getStartBytes(), startIndex))
                throw new IORuntimeException("分片连续性异常：" + JSONUtil.toJsonStr(chunkInfo));
            // 设定下一个起始索引
            startIndex = chunkInfo.getEndBytes().intValue() + 1;
        }
        // 长度校验
        if (fileLength > 0 && ObjectUtil.notEqual(fileLength, startIndex - 1))
            throw new IORuntimeException(String.format("分片总长与文件长度不一致: fileLength: %s, chunksLength: %s",
                fileLength, startIndex - 1));
        log.debug("文件分片合并校验通过: {}", chunksInfo);
    }

    // ================================================ 索引型分片工具 ================================================ //

    private InxChunkMetadata checkInxChunks(FileStorage fileStorage, String chunkSuperPath) {
        // 索引型分片元数据检查
        InxChunkMetadata chunkMetadata = this.readMetadata(chunkSuperPath);
        if (chunkMetadata == null)
            throw new IORuntimeException(new FileNotFoundException("分片操作元数据未找到"));
        if (StrUtil.isNotBlank(fileStorage.getFileName()))
            chunkMetadata.setFileName(fileStorage.getFileName());
        if (StrUtil.isNotBlank(fileStorage.getSuperPath()))
            chunkMetadata.setSuperPath(fileStorage.getSuperPath());
        if (fileStorage.getLength() != null)
            chunkMetadata.setLength(fileStorage.getLength());
        if (StrUtil.isNotBlank(fileStorage.getDigest()))
            chunkMetadata.setDigest(fileStorage.getDigest());
        if (StrUtil.isNotBlank(fileStorage.getDigestType()))
            chunkMetadata.setDigestType(fileStorage.getDigestType());

        int totalChunks = chunkMetadata.getTotalChunks() == null ? 0 : chunkMetadata.getTotalChunks();
        if (totalChunks <= 0)
            throw new IORuntimeException("chunks metadata error，totalChunks is Less than or equal to 0!");
        int existChunks = this.countInxChunks(chunkSuperPath);
        if (existChunks != totalChunks)
            throw new IORuntimeException("file chunks is incomplete!");
        return chunkMetadata;
    }

    /**
     * 写入文件元数据
     * 文件不存在时自动创建
     *
     * @param chunkInfo 分片信息
     */
    private void writeMetadata(InxChunkStorage chunkInfo) {
        InxChunkMetadata chunkMetadata = this.readMetadata(chunkInfo.getSuperPath());
        if (chunkMetadata != null) {
            boolean unchanged = Objects.equals(chunkMetadata.getTotalChunks(), chunkInfo.getTotalChunks())
                && Objects.equals(chunkMetadata.getLength(), chunkInfo.getTotalSize());
            if (unchanged)
                return;
        }
        if (chunkMetadata == null)
            chunkMetadata = new InxChunkMetadata();
        chunkMetadata.setTotalChunks(chunkInfo.getTotalChunks());
        chunkMetadata.setLength(chunkInfo.getTotalSize());
        chunkMetadata.setChunkSize(chunkInfo.getChunkSize());
        this.writeMetadata(chunkInfo.getSuperPath(), chunkMetadata);
    }

    /**
     * 获取已上传的分片数量
     *
     * @param supPath
     * @return
     */
    private int countInxChunks(String supPath) {
        String pattern = "^(\\d+)$";
        File[] files = FileUtil.file(supPath).listFiles(file -> file.getName().trim().matches(pattern));
        return files == null ? 0 : files.length;
    }

}
